/*
    YmsgIRC - Program enabling YMSG chat clients to use IRC
    Copyright (C) 2011 Crash Daemonicus ( crashenator -at- gmail.com )

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
#include "network.h"

#define NwNotifyType_Dbg(t,v) if(t==v) return string(#v);

string NwNotifyType_Debug(NwNotifyType t)
{
    //#ifdef COMMON_DBG
    NwNotifyType_Dbg(t,NW_NONE);
    NwNotifyType_Dbg(t,NW_CONNINFO);
    NwNotifyType_Dbg(t,NW_CONNECT);
    NwNotifyType_Dbg(t,NW_DISCONNECT);
    NwNotifyType_Dbg(t,NW_LOGIN);
    NwNotifyType_Dbg(t,NW_LOGOUT);
    NwNotifyType_Dbg(t,NW_USERNAME);
    NwNotifyType_Dbg(t,NW_PASSWORD);
    NwNotifyType_Dbg(t,NW_ULIST_START);
    NwNotifyType_Dbg(t,NW_ULIST_NAME);
    NwNotifyType_Dbg(t,NW_ULIST_TOPIC);
    NwNotifyType_Dbg(t,NW_ULIST_APPEND);
    NwNotifyType_Dbg(t,NW_ULIST_REMOVE);
    NwNotifyType_Dbg(t,NW_ULIST_END);
    NwNotifyType_Dbg(t,NW_PRIVMSG);
    NwNotifyType_Dbg(t,NW_CHATMSG);
    NwNotifyType_Dbg(t,NW_DATA);
    //#else
    //return string("NW_")+itos(t);
    //#endif
    return string("NW_???");
}


NwNotify::NwNotify(NwNotifyType type){typ=type;}
NwNotify::NwNotify(NwNotifyType type,string s1,string s2,string s3){sSet(type,s1,s2,s3);}
const NwNotify& NwNotify::sSet(NwNotifyType type,string s1,string s2,string s3)
{
    typ=type;
    sDat[0]=s1;sDat[1]=s2;sDat[2]=s3;
    return *this;
}
NwConnection::NwConnection()
{
    dbg_head("NwConnection::NwConnection()");
    sock=INVALID_SOCKET;
    blocked=NW_BLOCKED_UNKNOWN;//if the socket state is reliable, this concept can be changed.
    ixBuf=sizeof(buf)-1;
    iwBuf=0;
    buf_nullterminated=false;
    error=0;
    dbg_foot(0);
}
void NwConnection::setBufLimit(size_t length){
    if(length==-1) length=sizeof(buf);
    ixBuf=length-1;
}
size_t NwConnection::getBufLimit(){return ixBuf+1;}
int NwConnection::block(bool state)
{
    if(state && blocked==NW_BLOCKED) return 0;//already blocked
    if(!state && blocked==NW_UNBLOCKED) return 0;//already unblocked
    if(state) blocked=NW_BLOCKED;
    else blocked=NW_UNBLOCKED;
    return XSABlock(sock,state);//error checking?
}
size_t NwConnection::lAvail(size_t bytes) const
{
    size_t maxwrite=(ixBuf-iwBuf) + 1;
    if(bytes==0 || bytes>maxwrite) return maxwrite;
    else return bytes;
}
int NwConnection::iReset(bool conditional)
{
    if(!conditional || (conditional && iwBuf>ixBuf)) iwBuf=0;
    return iwBuf;
}
int NwConnection::iForward(size_t bytes,bool limit)
{
    if(limit) bytes=lAvail(bytes);
    iwBuf+=bytes;
    iReset(true);
    return iwBuf;
}
int NwConnection::iLastWritten() const
{
    int ilwBuf=iwBuf-1;
    if(ilwBuf<0) return ixBuf;
    return ilwBuf;
}
int NwConnection::get(size_t bytes,bool blocking)
{
    if(error!=0 && error!=XSAEWOULDBLOCK) return SOCKET_ERROR;
    bytes=lAvail(bytes);
    block(blocking);//error checking?
    int rBytes=recv(sock,&buf[iwBuf],bytes,0);//error checking?
    if(rBytes==SOCKET_ERROR){
		//dbgline("E:Recv");
        error=XSAGetLastError();
    }
    else{
        error=0;
        if(buf_nullterminated)
        {
            int iTerm=iwBuf+rBytes;
            if(iCheckLimit(iTerm,0,sizeof(buf)-1))
            {
                buf[iTerm]=0;
            }
        }
        iForward(rBytes);
    }
    return rBytes;
}
bool NwConnection::put(const char* buf,size_t bytes,bool blocking)
{
    if(error!=0 && error!=XSAEWOULDBLOCK) return false;
    if(bytes==0) bytes=strlen(buf);
    block(blocking);//error checking?
    int err=send(sock,buf,bytes,0);
    bool cond=(err==SOCKET_ERROR);
	if(cond){
		dbgline("E:Send");
		error=XSAGetLastError();
	}
    else error=0;
    return !cond;
}
bool NwConnection::put(const string& buf,size_t bytes,bool blocking)
{
    return put(buf.c_str(),bytes,blocking);
}
bool NwConnection::set(const string& host,unsigned short uPort)
{
    dbg_head("NwConnection::set(const string& host,unsigned short uPort)");
    hostname=host;
    if(!XSASinFromName(host, sin))//error checking?
    {
        dbgline("E:XSASinFromName");
    }
    ip=inet_ntoa(sin.sin_addr);//error checking?
    port=uPort;
    sin.sin_port = htons(port);
    ReturnD(true);
}
bool NwConnection::open()
{
	dbg_head("NwConnection::open()");
    error=0;blocked=NW_BLOCKED_UNKNOWN;
    sock=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//INVALID_SOCKET
    if(sock==INVALID_SOCKET) {return false;}
    int ret=connect(sock, (SOCKADDR*) &sin, sizeof(sin));//SOCKET_ERROR
    ReturnD(ret!=SOCKET_ERROR);
}
bool NwConnection::close()
{
	dbg_head("NwConnection::close()");
    error=0;blocked=NW_BLOCKED_UNKNOWN;
    closesocket(sock);//error checking?
    sock=INVALID_SOCKET;
    ReturnD(true);
}
bool NwConnection::listn(int iPending)
{
    dbg_head("NwConnection::listn(int iPending)");
    error=0;blocked=NW_BLOCKED_UNKNOWN;
    if(iPending==-1) iPending=SOMAXCONN;
    sock=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//INVALID_SOCKET
    if(sock==INVALID_SOCKET){ ReturnX(false,"socket error"); }
    int ret=bind(sock, (SOCKADDR*) &sin, sizeof(sin));//SOCKET_ERROR
    if(ret==SOCKET_ERROR){
        dbgline(itos(XSAGetLastError()));
        ReturnX(false,"bind error"); 
    }
    ret=listen(sock, iPending);//SOCKET_ERROR
    ReturnD(ret!=SOCKET_ERROR);
}
bool NwConnection::acceptFrom(const SOCKET& sockListen)
{
    error=0;blocked=NW_BLOCKED_UNKNOWN;
    sock=accept(sockListen,NULL,NULL);
    return (sock!=INVALID_SOCKET);
}
bool NwConnection::acceptFrom(const NwConnection& listener)
{
    return acceptFrom(listener.sock);
}




void NwNetwork::stepActions()
{
    
    if(actions.size()<1) {return;}
    NwNotify n=actions.front();
    
    puts(name+"~" + string(":ACTION:") + NwNotifyType_Debug(n.typ)
        +" "+n.sDat[0]+" "+n.sDat[1]+" "+n.sDat[2]
    );
    
    bool check_origin = (n.cDat[0]==NW_CHECK_USERNAME);
    bool check_channel = (n.cDat[0]==NW_CHECK_CHANNEL);
    bool check_destination = (n.cDat[1]==NW_CHECK_USERNAME);
    
    switch(n.typ)
    {
        case NW_CONNINFO:
            connection.set(n.sDat[0],n.iDat[0]);
            break;
        case NW_USERNAME:
            if(check_origin) username=n.sDat[1];
            break;
        case NW_PASSWORD:
            password=n.sDat[0];
            break;
        case NW_ULIST_START:
            channel.reset();
            channel.name=n.sDat[0];
            channel.topic=n.sDat[1];
            break;
        case NW_ULIST_NAME:
            channel.name=n.sDat[0];
            break;
        case NW_ULIST_TOPIC:
            if(check_channel) channel.topic=n.sDat[1];
            break;
        case NW_ULIST_APPEND:
			if(check_channel) channel.users.push_back(n.sDat[1]);
            break;
        case NW_ULIST_REMOVE:
			if(check_channel) channel.users.remove(n.sDat[1]);
            break;
        case NW_CONNECT:
            connection.open();
            break;
        case NW_DISCONNECT:
            connection.close();
            break;
        case NW_ULIST_END://all of these are network specific.
        case NW_LOGOUT:
        case NW_LOGIN:
        case NW_DATA:
        default:
            break;
    }
    actions.pop();
}
void NwNetwork::stepRecv(){;}
void NwNetwork::step(NwStepState state){
    stepActions();stepRecv();
}
void NwNetwork::notifyStateText(const string& state){;}


NwCheckType NwNetwork::CheckOrigin(string u, bool defblank)
{
    if(u=="")
    {
        if(defblank) return NW_CHECK_USERNAME;
        else return NW_CHECK_BLANK;
    }
    if(u==username) return NW_CHECK_USERNAME;
    if(u==channel.name) return NW_CHECK_CHANNEL;
    if(u==NW_RESERVED_USERNAME) return NW_CHECK_RESERVED;
    if(u[0]==NW_RESERVED_CHANPREFIX) return NW_CHECK_CHANNEL_UNKNOWN;
    return NW_CHECK_UNKNOWN;
}



void NwNetwork::NwAConnInfo(const string& host, unsigned int port)
{
    NwNotify n(NW_CONNINFO,host,"","");
    n.iDat[0]=port;
    actions.push(n);
}


void NwNetwork::NwEAccept(const NwConnection& listener, bool conditionalEvt)
{
    bool ready=(connection.sock==INVALID_SOCKET);
    bool done=false;
        //if(ready) dbgline("ready accept");
    if(ready) done=connection.acceptFrom(listener);
    if(done) dbgline("accept done");
    if(!conditionalEvt || done) events.push(NwNotify(NW_CONNECT));
}
void NwNetwork::NwEConnect(bool conditionalEvt)
{
    bool ready=(connection.sock==INVALID_SOCKET);
    bool done=false;
    if(ready) done=connection.open();
    if(!conditionalEvt || done) events.push(NwNotify(NW_CONNECT));
    
}
void NwNetwork::NwEDisconnect(bool conditionalEvt)
{
    bool ready=(connection.sock!=INVALID_SOCKET);
    bool done=false;
    if(ready) dbgline("ready disconnect");
	if(ready){ done=connection.close(); }
    if(done) dbgline("disconnect done");
    if(!conditionalEvt || done) events.push(NwNotify(NW_DISCONNECT));
}
void NwNetwork::NwEUsername(string from,string to)
{
    NwNotify n(NW_USERNAME,from,to,"");
    n.cDat[0]=CheckOrigin(from);
    n.cDat[1]=CheckOrigin(to);
    if(n.cDat[0]==NW_CHECK_USERNAME) username=to;
    events.push(n);
}
void NwNetwork::NwEUsername(string to)
{
    return NwEUsername("",to);
}
void NwNetwork::NwEChanName(string c)
{
    if(c=="") return;
    channel.name=c;
    NwNotify n(NW_ULIST_NAME,c,"","");
    n.cDat[0]=CheckOrigin(c);
    events.push(n);
}
void NwNetwork::NwEChanTopic(string c, string t)
{
    channel.topic=t;
    NwNotify n(NW_ULIST_TOPIC,c,t,"");
    n.cDat[0]=CheckOrigin(c);
    events.push(n);
}
void NwNetwork::NwEChanStart(string c, string t)
{
    channel.reset();
    if(c=="") return;
    channel.name=c;
    channel.topic=t;
    NwNotify n(NW_ULIST_START,c,t,"");
    n.cDat[0]=CheckOrigin(c);
    events.push(n);
}
bool NwNetwork::NwEChanAppend(string c, string u)
{
    if(c=="" || channel.name==c)
    {
        channel.users.push_back(u);
        NwNotify n(NW_ULIST_APPEND,c,u,"");
        n.cDat[0]=CheckOrigin(c);
        n.cDat[1]=CheckOrigin(u);
        events.push(n);
        return true;
    }
    return false;
}
bool NwNetwork::NwEChanRemove(string c, string u)
{
    if(c=="" || channel.name==c)
    {
        channel.users.remove(u);
        NwNotify n(NW_ULIST_REMOVE,c,u,"");
        n.cDat[0]=CheckOrigin(c);
        n.cDat[1]=CheckOrigin(u);
        events.push(n);
        return true;
    }
    return false;
}
void NwNetwork::NwEChanReplace(string c, string from, string to)
{
    if(NwEChanRemove(c, from)) NwEChanAppend(c, to);
}
void NwNetwork::NwEChanEnd(string c)
{
    if(c=="" || channel.name==c)
    {
        //...?
        NwNotify n(NW_ULIST_END,c,"","");
        n.cDat[0]=CheckOrigin(c);
        events.push(n);
    }
}

const NwNotify& NwNetwork::NwEMsg(string src,string dst, string txt,int info)
{
    NwNotify n(NW_PRIVMSG);
    n.sDat[0]=src;n.cDat[0]=CheckOrigin(src);n.iDat[0]=info;
    n.sDat[1]=dst;n.cDat[1]=CheckOrigin(dst);
    n.sDat[2]=txt;
    if(n.cDat[1]==NW_CHECK_CHANNEL || n.cDat[1]==NW_CHECK_CHANNEL_UNKNOWN) n.typ=NW_CHATMSG;
    events.push(n);
    return events.back();
}

